agentmux_srv\backend\process_tracker/mod.rs
1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Agent-spawned process tracking.
5//!
6//! Gives the host a complete, authoritative view of what each agent CLI
7//! has forked — backgrounded shells, dev servers, Docker containers,
8//! file watchers, nested bash/python/node children, etc. The goal is
9//! end-user visibility: a user running multiple agents can see in one
10//! place what's still running on their machine, and kill it reliably
11//! when they're done.
12//!
13//! The API is a platform-agnostic trait. Per-platform impls use the
14//! strongest available mechanism:
15//!
16//! | Platform | Impl | Mechanism | Confidence |
17//! |----------|-----------------|--------------------------------------------|------------|
18//! | Windows | `JobObjectTracker` | `CreateJobObject` + `AssignProcessToJobObject` + `TerminateJobObject` | high |
19//! | Linux | `Cgroupv2Tracker` | `systemd-run --user --scope` + `cgroup.procs` / `cgroup.kill` | high |
20//! | macOS | `ProcessGroupTracker` | `POSIX_SPAWN_SETPGROUP` + `killpg` | best-effort |
21//! | other | `StubTracker` | no-op | none |
22//!
23//! The frontend's swarm panel surfaces the confidence level so users know
24//! when tracking may miss escaped descendants.
25//!
26//! See `agentmux-ai/AGENT_SPAWNED_PROCESSES_SPEC.md` for the design.
27
28use std::sync::Arc;
29
30pub mod registry;
31
32#[cfg(windows)]
33pub mod windows;
34
35// `pub mod stub;` (file-form) was here. Removed: `stub.rs` doesn't exist
36// in the tree — only the two inline `pub mod stub { ... }` definitions
37// below (cfg(not(windows)) and cfg(windows)) define the module. On Linux
38// the file-form line collided with the inline non-Windows definition →
39// E0428 "the name `stub` is defined multiple times" → broke `task dev`.
40
41/// A single process tracked by the host — PID + metadata enriched
42/// per-platform. The frontend renders one row per entry.
43#[derive(Debug, Clone, serde::Serialize)]
44pub struct TrackedProcess {
45 pub pid: u32,
46 /// Full command line, or best approximation. May be empty if the
47 /// platform doesn't expose it cheaply (macOS without `libproc`).
48 pub command: String,
49 /// Working-set / RSS in bytes. 0 if unavailable.
50 pub rss_bytes: u64,
51 /// Unix ms of process creation, 0 if unavailable.
52 pub started_at_ms: u64,
53}
54
55/// Opaque per-agent handle returned by the tracker when we wrap a spawn.
56/// Held inside `AgentProcessRegistry` for the lifetime of the pane;
57/// dropped when the pane closes or the agent exits.
58#[allow(dead_code)]
59pub trait TrackerHandle: Send + Sync {
60 /// Add a freshly-spawned process to the tracked tree. Called by the
61 /// controller immediately after `tokio::process::Command::spawn`.
62 /// Descendants created AFTER this call are caught automatically;
63 /// descendants created BEFORE (in the ~1ms race window) escape.
64 /// No-op in the stub impl — platforms without a real tracker
65 /// silently accept the PID and move on.
66 fn assign_process(&self, pid: u32) -> Result<(), String>;
67
68 /// Enumerate the current members of this tracked tree.
69 ///
70 /// Must be cheap enough to poll every ~2s. On Windows this is a
71 /// single Job Object query; on Linux it's a read of `cgroup.procs`;
72 /// on macOS it's a sysctl scan.
73 fn list_members(&self) -> Vec<TrackedProcess>;
74
75 /// Forcibly terminate every process in this tracked tree.
76 fn kill_tree(&self);
77
78 /// Terminate a single process by PID, if it's a member of this tree.
79 /// Returns `true` if the PID was known and the kill was attempted.
80 fn kill_pid(&self, pid: u32) -> bool;
81
82 /// Describes how confidently this platform tracks descendants.
83 /// Surfaced to the UI so the user can tell when tracking is
84 /// best-effort and escape-prone.
85 fn confidence(&self) -> TrackingConfidence;
86}
87
88/// How reliable this platform's tracker is.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
90#[serde(rename_all = "snake_case")]
91pub enum TrackingConfidence {
92 /// Descendants can't escape the tracker. Windows Job Objects +
93 /// Linux cgroups v2.
94 High,
95 /// Descendants can escape via `setsid`, launchd, etc. macOS.
96 BestEffort,
97 /// Platform has no tracker. No guarantees.
98 None,
99}
100
101/// Factory: returns a platform-appropriate tracker handle that will
102/// accept the next-spawned process and everything it forks.
103///
104/// Call once per agent pane; reuse the handle across multiple turns of
105/// the `SubprocessController` so children from any turn are all tracked
106/// under the same umbrella.
107pub fn new_tracker(block_id: &str) -> Arc<dyn TrackerHandle> {
108 #[cfg(windows)]
109 {
110 match windows::JobObjectTracker::new(block_id) {
111 Ok(t) => Arc::new(t),
112 Err(e) => {
113 tracing::warn!(
114 block_id = %block_id,
115 error = %e,
116 "[process-tracker] JobObjectTracker init failed — falling back to stub"
117 );
118 Arc::new(stub::StubTracker)
119 }
120 }
121 }
122 #[cfg(not(windows))]
123 {
124 let _ = block_id;
125 Arc::new(stub::StubTracker)
126 }
127}
128
129#[cfg(not(windows))]
130pub mod stub {
131 //! No-op tracker used on unsupported platforms or when init fails.
132 //! All operations succeed silently; `list_members` always returns
133 //! empty. Confidence reports `None` so the UI can inform the user
134 //! that tracking is disabled.
135
136 use super::{TrackedProcess, TrackerHandle, TrackingConfidence};
137
138 pub struct StubTracker;
139
140 impl TrackerHandle for StubTracker {
141 fn assign_process(&self, _pid: u32) -> Result<(), String> {
142 Ok(())
143 }
144 fn list_members(&self) -> Vec<TrackedProcess> {
145 Vec::new()
146 }
147 fn kill_tree(&self) {}
148 fn kill_pid(&self, _pid: u32) -> bool {
149 false
150 }
151 fn confidence(&self) -> TrackingConfidence {
152 TrackingConfidence::None
153 }
154 }
155}
156
157#[cfg(windows)]
158pub mod stub {
159 //! Windows fallback if `JobObjectTracker::new` fails (e.g. the
160 //! process is not elevated enough to create a job object). The real
161 //! impl lives in `windows`; this is only used for the init-fail
162 //! recovery path.
163
164 use super::{TrackedProcess, TrackerHandle, TrackingConfidence};
165
166 pub struct StubTracker;
167
168 impl TrackerHandle for StubTracker {
169 fn assign_process(&self, _pid: u32) -> Result<(), String> {
170 Ok(())
171 }
172 fn list_members(&self) -> Vec<TrackedProcess> {
173 Vec::new()
174 }
175 fn kill_tree(&self) {}
176 fn kill_pid(&self, _pid: u32) -> bool {
177 false
178 }
179 fn confidence(&self) -> TrackingConfidence {
180 TrackingConfidence::None
181 }
182 }
183}